﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using ThreeByte.ActiveDeck.Publish.Model;

namespace ThreeByte.ActiveDeck.Publish
{
    public partial class ActivePublisher
    {
        /// <summary>
        /// Inner class helps encapsulate the details of the subscription and completion
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
            Justification = "Timer is disposed in the End method")]
        class SubscriptionAsyncResult : AsyncResult
        {
            static TimerCallback resumeOnTimeout = new TimerCallback(ResumeOnTimeout);
            ActivePublisher parent;
            Timer timer;
            bool timedOut;
            ActiveMessage bookmarkTriggerMessage;
            ActiveMessage[] messages;
            Bookmark[] bookmarks;
            object syncRoot = new object();

            public SubscriptionAsyncResult(ActivePublisher parent, IDictionary<string, int> subscriptions, TimeSpan timeout, AsyncCallback callback, object state)
                : base(callback, state) {
                this.parent = parent;


                foreach(string channel in (from s in subscriptions where s.Value == 0 select s.Key).ToArray()) {
                    subscriptions[channel] = this.parent.maxMessageId + 1;
                }

                if(this.QueryMessages(subscriptions) || this.CreateBookmarks(subscriptions, timeout)) {
                    this.Complete(true, null);
                }
            }

            public static IEnumerable<ActiveMessage> End(IAsyncResult result) {
                try {
                    SubscriptionAsyncResult r = AsyncResult.End<SubscriptionAsyncResult>(result);
                    return r.messages;
                } finally {
                    SubscriptionAsyncResult r1 = result as SubscriptionAsyncResult;
                    if(r1 != null && r1.timer != null) {
                        r1.timer.Dispose();
                        r1.timer = null;
                    }
                }
            }

            bool CreateBookmarks(IDictionary<string, int> subscriptions, TimeSpan timeout) {
                this.bookmarks = new Bookmark[subscriptions.Count];
                int bookmarkIndex = 0;
                this.parent.channelsLock.EnterUpgradeableReadLock();
                try {
                    foreach(KeyValuePair<string, int> subscription in subscriptions) {
                        PublishedChannel publishedChannel = null;
                        bool channelExisted = true;
                        if(!this.parent.Channels.TryGetValue(subscription.Key, out publishedChannel)) {
                            this.parent.channelsLock.EnterWriteLock();
                            try {
                                if(!this.parent.Channels.TryGetValue(subscription.Key, out publishedChannel)) {
                                    channelExisted = false;
                                    publishedChannel = new PublishedChannel();
                                    this.bookmarks[bookmarkIndex] = new Bookmark { Continuation = this, From = subscription.Value, PublishedChannel = publishedChannel };
                                    publishedChannel.Bookmarks.Add(this.bookmarks[bookmarkIndex]);
                                    bookmarkIndex++;
                                    this.parent.Channels.Add(subscription.Key, publishedChannel);
                                }
                            } finally {
                                this.parent.channelsLock.ExitWriteLock();
                            }
                        }
                        if(channelExisted) {
                            publishedChannel.Lock.EnterWriteLock();
                            try {
                                int last = publishedChannel.Messages.Count > 0 ? publishedChannel.Messages.Last().ActiveMessage.MessageId : -1;
                                if(subscription.Value <= last) {
                                    List<ActiveMessage> result = new List<ActiveMessage>(1);
                                    this.AddMatchingMessages(subscription.Value, publishedChannel, result);
                                    this.messages = result.ToArray();
                                    break;
                                } else {
                                    this.bookmarks[bookmarkIndex] = new Bookmark { Continuation = this, From = subscription.Value, PublishedChannel = publishedChannel };
                                    publishedChannel.Bookmarks.Add(this.bookmarks[bookmarkIndex]);
                                    bookmarkIndex++;
                                }
                            } finally {
                                publishedChannel.Lock.ExitWriteLock();
                            }
                        }
                    }
                } finally {
                    this.parent.channelsLock.ExitUpgradeableReadLock();
                }
                if(this.messages == null) {
                    this.timer = new Timer(resumeOnTimeout, this, timeout, TimeSpan.Zero);
                }

                return this.messages != null;
            }

            void DeleteBookmarks() {
                for(int i = 0; this.bookmarks != null && i < this.bookmarks.Length && this.bookmarks[i] != null; i++) {
                    this.bookmarks[i].PublishedChannel.Lock.EnterWriteLock();
                    try {
                        this.bookmarks[i].PublishedChannel.Bookmarks.Remove(this.bookmarks[i]);
                    } finally {
                        this.bookmarks[i].PublishedChannel.Lock.ExitWriteLock();
                    }
                }
            }

            bool QueryMessages(IDictionary<string, int> subscriptions) {
                List<ActiveMessage> cummulativeResult = new List<ActiveMessage>(1);
                foreach(KeyValuePair<string, int> subscription in subscriptions) {
                    PublishedChannel publishedChannel = null;
                    this.parent.channelsLock.EnterReadLock();
                    try {
                        if(this.parent.Channels.TryGetValue(subscription.Key, out publishedChannel)) {
                            publishedChannel.Lock.EnterReadLock();
                            try {
                                this.AddMatchingMessages(subscription.Value, publishedChannel, cummulativeResult);
                            } finally {
                                publishedChannel.Lock.ExitReadLock();
                            }
                        }
                    } finally {
                        this.parent.channelsLock.ExitReadLock();
                    }
                }
                if(cummulativeResult.Count > 0) {
                    this.messages = cummulativeResult.ToArray();
                }

                return this.messages != null;
            }

            void AddMatchingMessages(int from, PublishedChannel publishedChannel, List<ActiveMessage> result) {
                int i;
                for(i = publishedChannel.Messages.Count;
                    i > 0 && publishedChannel.Messages[i - 1].ActiveMessage.MessageId >= from;
                    i--) ;
                for(; i < publishedChannel.Messages.Count; i++) {
                    result.Add(publishedChannel.Messages[i].ActiveMessage);
                }
            }

            static void ResumeOnTimeout(object state) {
                ((SubscriptionAsyncResult)state).ResumeOnTimeout();
            }

            void ResumeOnTimeout() {
                bool completeSelf = false;
                lock(this.syncRoot) {
                    if(!this.IsCompleted && !this.timedOut && this.bookmarkTriggerMessage == null) {
                        this.timedOut = true;
                        completeSelf = true;
                    }
                }
                if(completeSelf) {
                    this.DeleteBookmarks();
                    this.Complete(false, null);
                }
            }

            public void ResumeOnBookmark(ActiveMessage message) {
                bool completeSelf = false;
                lock(this.syncRoot) {
                    if(!this.IsCompleted && !this.timedOut && this.bookmarkTriggerMessage == null) {
                        // TODO: is it possible for a bookmark to be resumed by more than one message? Should we re-query the channel?
                        this.bookmarkTriggerMessage = message;
                        completeSelf = true;
                    }
                }
                if(completeSelf) {
                    this.messages = new ActiveMessage[] { this.bookmarkTriggerMessage };
                    this.DeleteBookmarks();
                    this.Complete(false, null);
                }
            }
        }




    }
}